Skip to content

feat: server-to-server Outlook/Exchange calendar sync#41326

Draft
milton-rucks wants to merge 18 commits into
developfrom
feat/server-calendar-sync
Draft

feat: server-to-server Outlook/Exchange calendar sync#41326
milton-rucks wants to merge 18 commits into
developfrom
feat/server-calendar-sync

Conversation

@milton-rucks

Copy link
Copy Markdown

Proposed changes (including videos or screenshots)

Adds a server-to-server calendar integration with Microsoft Outlook/Exchange (Premium, outlook-calendar module). The server periodically fetches users' calendars using admin-provided credentials and imports them through the existing calendar service — users no longer need to authenticate against Outlook from their own clients. The legacy client/desktop-based integration is untouched and keeps working alongside it.

Two providers behind one abstraction (ICalendarSyncProvider):

  • Microsoft Graph (Exchange Online / M365): OAuth 2.0 client credentials — client secret or certificate credential (RS256 client assertion built with node:crypto) — calendarView delta queries with 410 recovery, Retry-After throttling backoff, getSchedule free/busy, and national clouds (GCC High / DoD). No MSAL/Graph SDK.
  • Exchange Web Services (on-prem 2016/2019/SE): service account with ApplicationImpersonation, NTLMv2 (vendored, incl. pure-TS MD4 — OpenSSL 3 removed MD4, so npm NTLM libs break on modern Node) or Basic auth over a pinned keep-alive socket, hand-rolled SOAP (FindItem/GetItem/GetUserAvailability/SyncFolderItems incremental sync) parsed with the repo's existing @xmldom/xmldom. Zero new npm dependencies.

Air-gap guarantee: with the EWS provider selected, no Microsoft cloud endpoint is ever contacted — enforced by construction and verified three ways in airGap.spec.ts (static source scan, factory-level check, runtime full-sync URL assertion).

Sync engine: cron-scheduled (configurable interval), batched with bounded parallelism, per-user sync state (calendar_sync_state collection) with delta-token lifecycle, idempotent upserts through CalendarService.import keyed on external id with a provider discriminator + iCalUId, deletion/cancellation handling via snapshot diffing and delta removals, mailbox mapping via verified email or a user custom field, and optional role scoping.

Modes:

  • full-events (default): subjects/times/bodies, matching legacy behavior.
  • free-busy-only: drives presence from availability blocks without ingesting any event details (needs only Calendars.ReadBasic on Graph — verified least-privileged permission for getSchedule).

Presence reuses the exact legacy mechanism (Presence.setActiveState claim with statusId: 'calendar'), so manual-DND protection and prior-status restore come from the presence engine — no parallel pathway.

Admin surface: new Calendar_Sync settings group (secrets as type: 'password', secret: true), a Test connection action button, and REST endpoints gated by a new manage-calendar-sync permission: calendar-sync.test-connection (optional probe mailbox proves impersonation/consent with actionable error codes), calendar-sync.run, calendar-sync.status, calendar-sync.user-status.

Optional Graph webhooks (off by default): per-user change-notification subscriptions created/renewed piggybacked on sync runs; the public receiver validates the handshake and authenticates every notification against a per-subscription clientState secret before triggering a single-user delta sync. Polling always continues — webhooks are an optimization, never a dependency.

Admin guide: docs/features/server-calendar-sync.md (Entra ID registration, admin consent, ApplicationAccessPolicy scoping, EWS service account + New-ManagementRoleAssignment, TLS/custom CA guidance, troubleshooting codes).

Issue(s)

N/A — internal feature work.

Steps to test or reproduce

  1. On a Premium-licensed workspace, open Admin → Settings → Calendar Sync.
  2. Graph: register an Entra ID app with the Calendars.Read application permission + admin consent; fill in tenant/client id and a secret (or certificate + private key); click Test connection.
  3. EWS: create a service account with ApplicationImpersonation, set the EWS endpoint URL/credentials (NTLM default); click Test connection.
  4. Enable the sync; events for users with verified emails matching tenant mailboxes appear via calendar-events.list and in the calendar UI, and busy meetings set users to busy (manual DND is never overridden).
  5. Inspect GET /api/v1/calendar-sync.status and calendar-sync.user-status?userId=… for diagnostics; POST /api/v1/calendar-sync.run triggers an immediate run.
  6. Unit tests: cd apps/meteor && yarn mocha --config ./.mocharc.js 'tests/unit/server/ee/calendarSync/*.spec.ts' (~130 tests, incl. RFC 1320 / [MS-NLMP] crypto vectors and the air-gap suite).

Further comments

  • Secrets follow the repo's existing model (masked in REST/audit, permission-gated) — there is no encryption-at-rest for settings anywhere in the codebase; flagged during design review.
  • Cross-path dedupe with the legacy desktop integration is best-effort only: desktop-pushed events carry no iCal UID and use EWS item ids, so the same meeting can appear twice if a user runs both paths; the admin guide recommends steering users off the client integration once server sync covers them.
  • The EWS transport intentionally bypasses serverFetch: NTLM authenticates the TCP connection, so the handshake must own a pinned keep-alive agent. TLS override is a scoped, explicit admin opt-in (never global); docs recommend NODE_EXTRA_CA_CERTS for private CAs.
  • CalendarService.import (community code) was extended additively to pass through provider/iCalUId; legacy behavior is unchanged.
  • Reuses the existing outlook-calendar license module (a new module would need SKU provisioning); confirm with product if a distinct module is preferred.
  • Follow-ups: API integration tests in tests/end-to-end/api/ (need a licensed instance) and a dedicated admin UI panel beyond the settings group.

🤖 Generated with Claude Code

Milton Rucks and others added 17 commits July 11, 2026 14:40
…ndar_sync_state model

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…port

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Client-credentials token manager (in-memory, expiry-aware, single-flight),
calendarView delta queries with 410 restart, Retry-After throttling backoff,
getSchedule free/busy, and log sanitization helpers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…and scheduler wiring

Batched per-user sync with delta-token lifecycle, full-snapshot deletion
diffing, cancellation handling, per-user sync state, and cron job that
re-deploys on settings changes (with state reset on semantic changes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ated startup wiring

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gine, mailbox resolver and sanitizer

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…xed Graph hosts, lint fixes and changeset

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mpersonation

Hand-rolled SOAP (FindItem/GetItem/GetUserAvailability/GetFolder) parsed with
the repo's existing @xmldom/xmldom; vendored NTLMv2 (incl. pure-TS MD4, absent
from OpenSSL 3) over a pinned keep-alive socket via node:https — zero new npm
dependencies. TLS override is a scoped, explicit admin opt-in. The provider
holds no Microsoft-cloud hostname (air-gap requirement).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ir-gap verification

Air-gap proven three ways: static source scan for Microsoft-cloud hostnames,
factory-level check that Graph is never constructed when provider=exchange-ews,
and a runtime full-sync run asserting the only contacted host is the configured
EWS endpoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nt ingestion

One getSchedule/GetUserAvailability call per user batch; merged contiguous
busy blocks set the same 'calendar' presence claim the legacy service uses
(manual DND protection and status restore come from the presence engine).
No event subjects or details are requested or stored in this mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… sync and diagnostics

calendar-sync.test-connection (optional probe mailbox proves impersonation/
calendar access), calendar-sync.run, calendar-sync.status and
calendar-sync.user-status — typed router, ajv schemas, manage-calendar-sync
permission and outlook-calendar license gate. Delta tokens are never exposed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…EWS impersonation) and changeset

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…coping and admin test-connection action

- GCC High / DoD (Azure Government) cloud endpoints, selectable per workspace
- OAuth client credentials via certificate (RS256 client assertion with x5t
  thumbprint, built with node:crypto — no new dependencies); private key is a
  multiline secret setting
- CalendarSync_User_Roles restricts sync to users holding the given roles
- Test-connection action button in the admin settings group backed by the
  calendarSyncTestConnection meteor method (manage-calendar-sync permission)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SyncState is established before the initial snapshot (no missed changes),
incremental runs page through Create/Update/Delete changes with client-side
window filtering (out-of-window updates are treated as deletions), and
ErrorInvalidSyncStateData falls back to a full snapshot — mirroring the Graph
delta 410 recovery. Mailboxes too large to fast-forward degrade gracefully to
windowed polling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion lifecycle

Per-user subscriptions on /users/{mailbox}/events are created and renewed
piggybacked on regular sync runs (no extra scheduler). The public receiver
endpoint answers the validation handshake as text/plain and authenticates
every notification against the per-subscription clientState secret before
triggering an immediate single-user delta sync. Notifications carry no
calendar data and webhooks remain a pure optimization over polling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dionisio-bot

dionisio-bot Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label
  • This PR is missing the required milestone or project

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@changeset-bot

changeset-bot Bot commented Jul 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5117a4a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@rocket.chat/meteor Minor
@rocket.chat/core-typings Minor
@rocket.chat/model-typings Minor
@rocket.chat/models Minor
@rocket.chat/rest-typings Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b4f454d2-04ba-469a-bea0-16907406a4b4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


Milton Rucks seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

? new https.Agent({
keepAlive: true,
maxSockets: 1,
...(config.allowSelfSignedCerts && { rejectUnauthorized: false }),
}

function hmacMd5(key: Buffer, data: Buffer): Buffer {
return crypto.createHmac('md5', key).update(data).digest();
throw new CalendarSyncError('invalid-certificate', 'The configured value is not a PEM-encoded certificate');
}
const der = Buffer.from(match[1].replace(/\s+/g, ''), 'base64');
return base64url(crypto.createHash('sha1').update(der).digest());
@codecov

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.38658% with 156 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.13%. Comparing base (edbaeef) to head (5117a4a).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop   #41326      +/-   ##
===========================================
+ Coverage    69.04%   69.13%   +0.09%     
===========================================
  Files         3757     3772      +15     
  Lines       147735   148673     +938     
  Branches     26395    26616     +221     
===========================================
+ Hits        102000   102782     +782     
- Misses       41234    41332      +98     
- Partials      4501     4559      +58     
Flag Coverage Δ
unit 70.61% <83.38%> (+0.11%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ge lab setup

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants